<?php

namespace tmcore\services\redis;

use think\facade\Cache;

/**
 * Redis
 * @package tmcore\utils
 */
class Redis {

    public \Redis $redisObj;//redis实例化时静态变量

    protected $sn;
    protected $index;

    /**
     * 初始化
     * @param array $options
     * @return \Redis
     */
    public function __construct($options=[]) 
    {
        $options = $options?$options:$this->getConfig();
        $host = trim(isset($options["host"]) ? $options["host"] : '127.0.0.1');
        $port = trim(isset($options["port"]) ? $options["port"] : 6379 );
        $auth = trim(isset($options["password"]) ? $options["password"] :"password" );
        $index = trim(isset($options["select"]) ? $options["select"] : 0 );
        $prefix = trim(isset($options["prefix"]) ? $options["prefix"] : "tm_");
        if (!is_integer($index) && $index>16) {
            $index = 0;
        }

        $sn = md5("{$host}{$port}{$auth}{$index}");
        $this->sn = $sn;
        if (!isset($this->redisObj)){
            $this->redisObj = new \Redis();
            $this->redisObj->connect($host,$port);
            $this->redisObj->auth($auth);
            $this->redisObj->select($index);
            if ($prefix !== '') {
                $this->redisObj->setOption(\Redis::OPT_PREFIX, $prefix);
            }
        }
        $this->redisObj->sn=$sn;
        $this->index = $index;
        return $this->redisObj;
    }

    /**
     * 获取redis配置
     * @return array
     */
    private static function getConfig(){
        $redisConfig = [
            'port' => '6379', // 端口号
            'timeout' => 1 // 超时
        ];

        $config = \think\facade\Config::get('redis');
        $config = $config?$config:Cache::getConfig()['stores']['redis'];
        return array_merge($redisConfig, $config);
    }

    /**
     * 检查链接
     * @return bool
     */
    public function checkConnect(){
        // redis配置信息
        $redisConfig = self::getConfig();

        try{
            $redis = new \Redis();
            $redis -> connect($redisConfig['host'], $redisConfig['port'], $redisConfig['timeout']);
            if($redisConfig['password']){
                $redis -> auth($redisConfig['password']);
            }
            $res = $redis -> ping(); // 检测当前链接状态，返回PONG或者抛出异常。
            if($res === '+PONG' || $res === true){
                return true;
            } else {
                return false;
            }
        }catch ( \Exception $e ){
            return false;
        }
    }

    /**
     * 获取redis对象
     * @param array $options 配置项
     * @return Redis
     */
    public static function instance($options=[])
    {
        $options = $options?$options:self::getConfig();
        return new Redis($options);
    }

    /**
     * 设置redis延时
     * @param string $key
     * @param int $time
     * @return bool
     */
    public function setExpire($key,$time = 0) {
        if(!$key){
            return false;
        }
        switch (true){
            case ($time==0):
                return $this->redisObj->expire($key,0);
                break;
            case ($time>time()):
                $this->redisObj->expireAt($key,$time);
                break;
            default:
                return $this->redisObj->expire($key,$time);
        }
    }

    /**
     * 队列数量
     * @param $key
     * @return int
     */
    public function lSize($key) {
        return $this->redisObj->lSize($key);
    }

    /*------------------------------------start 1.string结构----------------------------------------------------*/

    /**
     * 增,设置值,构建一个字符串
     * @param string $key 键值
     * @param string $value 设置值
     * @param int $timeOut 过期时间 0-永久
     * @return bool
     */
    public function set($key, $value, $timeOut=0) {
        $setRes =  $this->redisObj->set($key, $value);
        if ($timeOut > 0) $this->redisObj->expire($key, $timeOut);
        return $setRes;
    }

    /**
     * 查,获取 某键对应的值
     * @param string $key
     * @return bool|mixed|string
     */
    public  function get($key){
        $setRes =  $this->redisObj->get($key);//不存在返回false
        if($setRes === 'false'){
            return false;
        }
        return $setRes;
    }

    /*------------------------------------1.end string结构----------------------------------------------------*/



    /*------------------------------------2.start list结构----------------------------------------------------*/

    /**
     * 插入列表头部(先进后出,类似栈)
     * @param string $key 键值
     * @param string $value 设置值
     * @param int $timeOut 过期时间 0-永久
     * @return bool|int 列表长度
     */
    public function lpush($key,$value,$timeOut=0){
        $re = $this->redisObj->LPUSH($key,$value);
        if ($timeOut > 0) $this->redisObj->expire($key, $timeOut);
        return $re;
    }

    /**
     * 插入列表尾部(先进先出,类似队列)
     * @param string $key 键值
     * @param string $value 设置值
     * @param int $timeOut 过期时间 0-永久
     * @return bool|int 列表长度
     */
    public function rpush($key,$value,$timeOut=0){
        $re = $this->redisObj->RPUSH($key,$value);
        if ($timeOut > 0) $this->redisObj->expire($key, $timeOut);
        return $re;
    }

    /**
     * 获取列表所有数据（从头到尾取）
     * @param string $key 键值
     * @param int $head 开始
     * @param int $tail 结束
     * @return array
     */
    public function lrange($key,$head,$tail){
        return $this->redisObj->lrange($key,$head,$tail);
    }

    /**
     * 移除并返回列表 key 的尾元素
     * @param string $key 键值
     * @return bool|mixed
     */
    public function rpop($key){
        return $this->redisObj->rPop($key);
    }

    /**
     * 移除并返回列表的第一个元素
     * @param string $key 键值
     * @return bool|mixed
     */
    public function lpop($key){
        return $this->redisObj->lpop($key);
    }

    /*------------------------------------2.end list结构----------------------------------------------------*/



    /*------------------------------------3.start set结构----------------------------------------------------*/

    /**
     * 增，构建一个集合(无序集合)
     * @param string $key 集合Y名称
     * @param string $value 值
     * @param int $timeOut 过期时间 0-永久
     * @return bool|int 集合中的元素的数量
     */
    public function sadd($key,$value,$timeOut = 0){
        $re = $this->redisObj->sadd($key,$value);
        if ($timeOut > 0) $this->redisObj->expire($key, $timeOut);
        return $re;

    }

    /**
     * 查，取集合对应元素
     * @param string $key 值
     * @return array|bool 一组元素,集合的内容 顺序是随机的
     */
    public function smembers($key){
        $re =   $this->redisObj->exists($key);//存在返回1，不存在返回0
        if(!$re) return false;
        return $this->redisObj->smembers($key);
    }

    /*------------------------------------3.end  set结构----------------------------------------------------*/


    /*------------------------------------4.start sort set结构----------------------------------------------------*/

    /**
     * 增，改，构建一个集合(有序集合),支持批量写入,更新
     * @param string $key 集合名称
     * @param array $score_value key为scoll, value为该权的值
     * @param int $timeOut 过期时间 0-永久
     * @return bool|int
     */
    public function zadd($key,$score_value,$timeOut =0){
        if(!is_array($score_value)) return false;
        $a = 0;//存放插入的数量
        foreach($score_value as $score=>$value){
            $re =  $this->redisObj->zadd($key,$score,$value);//当修改时，可以修改，但不返回更新数量
            $re && $a+=1;
            if ($timeOut > 0) $this->redisObj->expire($key, $timeOut);
        }
        return $a;
    }

    /**
     * 查，有序集合查询，可升序降序,默认从第一条开始，查询一条数据
     * @param string $key 查询的键值
     * @param int $min 最小值,从第$min条开始
     * @param int $num 查询的条数
     * @param string $order asc表示升序排序,desc表示降序排序
     * @return array|bool 查询信息
     */
    public function zrange($key,$min = 0 ,$num = 1,$order = 'desc'){
        $re =   $this->redisObj->exists($key);//存在返回1，不存在返回0
        if(!$re) return false;//不存在键值
        if('desc' == strtolower($order)){
            $re = $this->redisObj->zrevrange($key,$min ,$min+$num-1);
        }else{
            $re = $this->redisObj->zrange($key,$min ,$min+$num-1);
        }
        if(!$re) return false;//查询的范围值为空
        return $re;
    }

    /**
     * 返回集合key中，成员member的排名
     * @param string $key 键值
     * @param string $member 成员值,scroll值
     * @param string $type
     * @return bool|int|string 其排名下标
     */
    public function zrank($key,$member,$type = 'desc'){
        $type = strtolower(trim($type));
        if($type == 'desc'){
            $re = $this->redisObj->zrevrank($key,$member);//其中有序集成员按score值递减(从大到小)顺序排列，返回其排位
        }else{
            $re = $this->redisObj->zrank($key,$member);//其中有序集成员按score值递增(从小到大)顺序排列，返回其排位
        }
        if(!is_numeric($re)) return false;//不存在键值
        return $re;
    }

    /**
     * 返回名称为key的zset中score >= star且score <= end的所有元素
     * @param string $key 键值
     * @param int $star 开始
     * @param int $end 结束
     * @return array 返回包含指定范围内的值的数组
     */
    public function zrangbyscore($key,$star,$end){
        return $this->redisObj->ZRANGEBYSCORE($key,$star,$end);
    }

    /**
     * 返回名称为key的zset中元素member的score
     * @param string $key 键值
     * @param string $member 成员值,scroll值
     * @return bool|float 返回指定排序集中给定成员的分数
     */
    function zscore($key,$member){
        return $this->redisObj->ZSCORE($key,$member);
    }

    /*------------------------------------4.end sort set结构----------------------------------------------------*/


    /*------------------------------------5.hash结构----------------------------------------------------*/

    /**
     * 增，以json格式插入数据到缓存,hash类型
     * @param array $redis_key $redis_key['key']数据库的表名称;$redis_key['field'],下标key
     * @param string $token 该活动的token，用于区分标识
     * @param string $id 该活动的ID，用于区分标识
     * @param array $data 要插入的数据
     * @param int $timeOut 过期时间,0-永久
     * @return bool|int 插入成功返回1,更新操作返回0
     */
    public function hsetJson($redis_key,$token,$id,$data,$timeOut = 0){
        $redis_table_name = $redis_key['key'].':'.$token;           //key的名称
        $redis_key_name = $redis_key['field'].':'.$id;              //field的名称，表示第几个活动
        $redis_info = json_encode($data);                           //field的数据value，以json的形式存储
        $re = $this->redisObj -> hSet($redis_table_name,$redis_key_name,$redis_info);//存入缓存
        if ($timeOut > 0) $this->redisObj->expire($redis_table_name, $timeOut);//设置过期时间
        return $re;
    }

    /**
     * 查，json形式存储的哈希缓存，有值则返回;无值则查询数据库并存入缓存
     * @param array $redis_key $redis['key'],$redis['field']分别是hash的表名称和键值
     * @param string $token 该活动的token，用于区分标识
     * @param string $id 该活动的ID，用于区分标识
     * @return bool|mixed 成功返回要查询的信息
     */
    public function hgetJson($redis_key,$token,$id){
        $re = $this->redisObj->hexists($redis_key['key'].':'.$token,$redis_key['field'].':'.$id);//返回缓存中该hash类型的field是否存在
        if($re){
            $info = $this->redisObj->hget($redis_key['key'].':'.$token,$redis_key['field'].':'.$id);
            $info = json_decode($info,true);
        }else{
            $info = false;
        }
        return $info;
    }

    /**
     * 为存储在键上的散列值添加一个值
     * @param string $redis_key 表名称
     * @param string $name 键值
     * @param string $data 存储值
     * @param int $timeOut 过期时间,0-永久
     * @return bool|int 插入成功返回1,更新操作返回0
     */
    public function hset($redis_key,$name,$data,$timeOut=0){
        $re = $this->redisObj  -> hset($redis_key,$name,$data);
        if ($timeOut > 0) $this->redisObj->expire($redis_key, $timeOut);
        return $re;
    }

    /**
     * 增，普通逻辑的插入hash数据类型的值
     * @param string $key 键名
     * @param array $data 一维数组，要存储的数据
     * @param int $timeOut 过期时间,0-永久
     * @return bool 更新和插入操作都返回OK
     */
    public function hmset($key,$data,$timeOut=0){
        $re = $this->redisObj  -> hmset($key,$data);
        if ($timeOut > 0) $this->redisObj->expire($key, $timeOut);
        return $re;
    }

    /**
     * 查，普通的获取值
     * @param string $key 表示该hash的下标值
     * @return array|bool
     */
    public function hval($key){
        $re =   $this->redisObj->exists($key);//存在返回1，不存在返回0
        if(!$re) return false;
        $vals = $this->redisObj -> hvals($key);
        $keys = $this->redisObj -> hkeys($key);
        $re = array_combine($keys,$vals);
        foreach($re as $k=>$v){
            if(!is_null(json_decode($v))){
                $re[$k] = json_decode($v,true);//true表示把json返回成数组
            }
        }
        return $re;
    }

    /**
     * 从密钥中存储的散列获取值
     * @param string $key 键值
     * @param array $filed 字段值
     * @return array|bool|string 查询的信息,如果散列表不存在或者该键不存在
     */
    public function hget($key,$filed = []){
        if(empty($filed)){
            $re = $this->redisObj->hgetAll($key);
        }elseif(is_string($filed)){
            $re = $this->redisObj->hget($key,$filed);
        }elseif(is_array($filed)){
            $re = $this->redisObj->hMget($key,$filed);
        }
        if(!$re){
            return false;
        }
        return $re;
    }

    /**
     * 从密钥中存储的散列设置值
     * @param string $redis_key 键值
     * @param string $name 名称
     * @param array $data 一维数组，要存储的数据
     * @param int $timeOut 过期时间,0-永久
     * @return bool|int
     */
    public function hsetAll($redis_key,$name,$data,$timeOut=0){
        $re = $this->redisObj  -> hset($redis_key,$name,$data);
        if ($timeOut > 0) $this->redisObj->expire($redis_key, $timeOut);
        return $re;
    }

    /**
     * 删除一个或多个散列字段
     * @param string $redis_key 表名称
     * @param string $name 键值
     * @return bool|int 返回删除的键的数量,如果散列表不存在或者该键不存在
     */
    public function hdel($redis_key,$name){
        $re = $this->redisObj-> hdel($redis_key,$name);
        return $re;
    }

    /*------------------------------------end hash结构----------------------------------------------------*/


    /*------------------------------------其他结构----------------------------------------------------*/

    /**
     * 设置自增,自减功能
     * @param string $key 要改变的键值
     * @param int $num 改变的幅度，默认为1
     * @param string $member 类型是zset或hash，需要在输入member或filed字段
     * @param string $type 类型，default为普通增减,还有:zset,hash
     * @return bool|float|int 返回自增后的scroll整数
     */
    public function incre($key,$num = 1,$member = '',$type=''){
        $num = intval($num);
        switch(strtolower(trim($type))){
            case "zset":
                $re = $this->redisObj->zIncrBy($key,$num,$member);//增长权值
                break;
            case "hash":
                $re = $this->redisObj->hincrby($key,$member,$num);//增长hashmap里的值
                break;
            default:
                if($num > 0){
                    $re = $this->redisObj->incrby($key,$num);//默认增长
                }else{
                    $re = $this->redisObj->decrBy($key,-$num);//默认增长
                }
                break;
        }
        if($re) return $re;
        return false;
    }

    /**
     * 清除缓存
     * @param int $type 默认为0，清除当前数据库；1表示清除所有缓存
     */
    function flush($type = 0){
        if($type) {
            $this->redisObj->flushAll();//清除所有数据库
        }else{
            $this->redisObj->flushdb();//清除当前数据库
        }
    }

    /**
     * 检验某个键值是否存在
     * @param string $keys 键值
     * @param string $type 类型，默认为常规
     * @param string $field 字段值,若为hash类型,输入$field
     * @return bool|int 成功:1
     */
    public function exists($keys,$type = '',$field=''){
        switch(strtolower(trim($type))){
            case 'hash':
                $re = $this->redisObj->hexists($keys,$field);//有返回1，无返回0
                break;
            default:
                $re = $this->redisObj->exists($keys);
                break;
        }
        return $re;
    }

    /**
     * 开启事务
     */
    public function multi() {
        $this->redisObj->multi();
    }

    /**
     * 结束事务
     */
    public function exec() {
        return $this->redisObj->exec();
    }

    /**
     * 事务监听
     * @param $key
     */
    public function watch($key) {
        $this->redisObj->watch($key);
    }

    /**
     * 删除缓存
     * @param string $key 键值
     * @param string $type 类型，默认为常规，还有hash,zset
     * @param string $field $field,hash=>表示$field值，set=>表示value,zset=>表示value值，list类型特殊暂时不加
     * @return bool|int 删除的个数
     */
    public function delete($key,$type="default",$field = ''){
        switch(strtolower(trim($type))){
            case 'hash':
                $re = $this->redisObj->hDel($key,$field);//返回删除个数
                break;
            case 'set':
                $re = $this->redisObj->sRem($key,$field);//返回删除个数
                break;
            case 'zset':
                $re = $this->redisObj->zDelete($key,$field);//返回删除个数
                break;
            default:
                $re = $this->redisObj->del($key);//返回删除个数
                break;
        }
        return $re;
    }

    /**
     * 析构函数
     */
    function __destruct()
    {
        $this->redisObj->close();
    }

    /**
     * 魔术方法,有不存在的操作的时候执行
     * @param string $method 方法名
     * @param array $args 参数
     */
    public function __call($method, $args)
    {
        call_user_func_array([$this->redisObj, $method], $args);
    }
}